#include "PreCompile.hpp"
#include "Spawn.hpp"
#include "SpawnGlibInternal.hpp"
#include "../Shared/Utils.hpp"

namespace EGlib
{
  ////////////
  // Process  
  Process::Process(const Glib::ustring &executable_name)
  {
    this->executable_name = executable_name;
    
    inherit_env = true;
  }
  
  Glib::RefPtr<Process> Process::create(const Glib::ustring &executable_name)
  {
    return Glib::RefPtr<Process>(new Process(executable_name));
  }

  void Process::set_working_directory(const Glib::ustring &working_dir)
  {
    this->working_dir = working_dir;
  }
  
  void Process::add_argument(const Glib::ustring &arg)
  {
    args.push_back(arg);
  }
  
  void Process::add_arguments(const Glib::ArrayHandle<std::string> &args_to_add)
  {
    for(Glib::ArrayHandle<std::string>::iterator it = args_to_add.begin();
      it != args_to_add.end();++it)
    {
      args.push_back(*it);
    }
  }

  void Process::add_arguments_string(const Glib::ustring &args,ArgumentParseMethod parse_method)
  {
    switch(parse_method)
    {
    case ArgumentParseMethod_Bash:
      add_arguments(Glib::shell_parse_argv(args));
      break;

    case ArgumentParseMethod_Windows:
      add_arguments(BuildNG::shell_parse_windows_style(args));
      break;

    case ArgumentParseMethod_Native:
#if defined(UNIX)
      add_arguments(Glib::shell_parse_argv(args));
#elif defined(WINDOWS)
      add_arguments(BuildNG::shell_parse_windows_style(args));
#else
#error No platform symbol defined
#endif
      break;
    }
  }

  void Process::add_env(const Glib::ustring &var_name,const Glib::ustring &val)
  {
    env_vars[var_name] = val;
  }

  void Process::set_inherit_env(bool inherit_env)
  {
    this->inherit_env = inherit_env;
  }
  
  void Process::set_stdin_channel(Glib::RefPtr<Glib::IOChannel> stdin_channel)
  {
    this->stdin_provider = Glib::RefPtr<IProcessInputProvider>(
      new InputFromGIOChannel(stdin_channel));
  }

  void Process::set_stdout_channel(Glib::RefPtr<Glib::IOChannel> stdout_channel)
  {
    this->stdout_handler = Glib::RefPtr<IProcessOutputHandler>(
      new OutputToGIOChannel(stdout_channel));
  }

  void Process::set_stderr_channel(Glib::RefPtr<Glib::IOChannel> stderr_channel)
  {
    this->stderr_handler = Glib::RefPtr<IProcessOutputHandler>(
      new OutputToGIOChannel(stderr_channel));
  }

  void Process::set_stdin_provider(Glib::RefPtr<IProcessInputProvider> stdin_provider)
  {
    this->stdin_provider = stdin_provider;
  }

  void Process::set_stdout_handler(Glib::RefPtr<IProcessOutputHandler> stdout_handler)
  {
    this->stdout_handler = stdout_handler;
  }

  void Process::set_stderr_handler(Glib::RefPtr<IProcessOutputHandler> stderr_handler)
  {
    this->stderr_handler = stderr_handler;
  }
  
  int Process::spawn_sync()
  {
    ProcessRunner proc_runner;

    proc_runner.proc = this_ptr<Process>(); 
    
    return proc_runner.spawn_sync();
  }
  
  ///////////////
  // ProcessRunner
  void ProcessRunner::setup()
  {
    input_buffer = 0;
    pid = 0;
    stdin_fd = -1;
    stdout_fd = -1;
    stderr_fd = -1;
  }
  
  void ProcessRunner::cleanup()
  {
    delete [] input_buffer;
    
    if(stdin_fd != -1)
    {
      close(stdin_fd);
      stdin_fd= -1;
    }
    
    if(stdout_fd != -1)
    {
      close(stdout_fd);
      stdout_fd = -1;
    }
    
    if(stderr_fd != -1)
    {
      close(stderr_fd);
      stderr_fd = -1;
    }
      
    if(pid)
    {
      g_spawn_close_pid(pid);
      pid = 0;
    }

    for(int i = 0;i < (int)envp.size();i++)
      g_free(envp[i]);
    envp.clear();
  }
  
  int ProcessRunner::spawn_sync()
  {
    setup();
    
    try
    {
      Glib::ustring cur_working_dir;
      if(proc->working_dir == "")
        cur_working_dir = Glib::get_current_dir();
      else
        cur_working_dir = proc->working_dir;
        
      std::vector<gchar*> c_args;
      c_args.push_back((gchar*)proc->executable_name.c_str());
      for(int i = 0;i < (int)proc->args.size();i++)
      {
        c_args.push_back((gchar*)proc->args[i].c_str());
      }
      c_args.push_back(0);
      
      for(std::map<Glib::ustring,Glib::ustring>::iterator it = proc->env_vars.begin();
        it != proc->env_vars.end();++it)
      {
        envp.push_back(0);

        gchar *cur = g_strconcat(it->first.c_str(),"=",it->second.c_str(),NULL);
        if(!cur)
          throw std::bad_alloc();
        envp.back() = cur;
      }

      // if inherit_env is specified, add all variables from the current env,
      // that are not overridden in the env_vars array
      if(proc->inherit_env)
      {
        gchar **envv = g_listenv();
        try
        {
          for(gchar **cur_env = envv;*cur_env;cur_env++)
          {
            if(proc->env_vars.find(*cur_env) == proc->env_vars.end())
            {
              envp.push_back(0);

              Glib::ustring cur_env_val = Glib::getenv(*cur_env);

              gchar *cur = g_strconcat(*cur_env,"=",cur_env_val.c_str(),NULL);
              if(!cur)
                throw std::bad_alloc();

              envp.back() = cur;
            }
          }
          g_strfreev(envv);
        }
        catch(...)
        {
          g_strfreev(envv);
          throw;
        }
      }
      
      envp.push_back(0);
      
      int spawn_flags = 0;
      if(!proc->stdin_provider)
        spawn_flags |= G_SPAWN_CHILD_INHERITS_STDIN;
        
      spawn_flags |= G_SPAWN_DO_NOT_REAP_CHILD;
            
      GError *error = 0;
      
      assert(pid == 0);
      
      assert(stdin_fd == -1);
      assert(stdout_fd == -1);
      assert(stderr_fd == -1);

      gboolean res = g_spawn_async_with_pipes(
        cur_working_dir.c_str(),
        &*c_args.begin(),
        &*envp.begin(),
        (GSpawnFlags)spawn_flags,
        NULL,
        0,
        &pid,
        proc->stdin_provider ? &stdin_fd : 0,
        proc->stdout_handler ? &stdout_fd : 0,
        proc->stderr_handler ? &stderr_fd : 0,
        &error);
      if(!res)
      {
        throw Glib::Error(error);
      }

      assert(pid);

      main_context = Glib::MainContext::create();
      main_loop = Glib::MainLoop::create(main_context);
      
      GSource *source = g_child_watch_source_new(pid);
      g_source_set_callback(source,(GSourceFunc)on_process_quitted,this,NULL);
      g_source_attach(source,main_context->gobj());
      g_source_unref(source);
      
      if(proc->stdin_provider)
      {
        stdin_pipe = Glib::IOChannel::create_from_fd(stdin_fd);
        stdin_pipe->set_close_on_unref(true);
        stdin_fd = -1;

        Glib::RefPtr<Glib::IOSource> input_wanted = 
          stdin_pipe->create_watch(Glib::IO_OUT);
          
        input_buffer_size = stdin_pipe->get_buffer_size();
        input_buffer = new char[input_buffer_size];
        input_data_size = 0;
        input_read_ptr = 0;
          
        input_wanted->attach(main_context);
        input_wanted->connect(sigc::mem_fun(this,&ProcessRunner::on_stdin_wanted));
      }
      
      if(proc->stdout_handler)
      {
        stdout_pipe = Glib::IOChannel::create_from_fd(stdout_fd);
        stdout_pipe->set_close_on_unref(true);
        stdout_fd = -1;

        Glib::RefPtr<Glib::IOSource> output_available = 
          stdout_pipe->create_watch(Glib::IO_IN|Glib::IO_PRI);
          
        output_available->attach(main_context);
        output_available->connect(sigc::mem_fun(this,&ProcessRunner::on_stdout_available));
      }
      
      if(proc->stderr_handler)
      {
        stderr_pipe = Glib::IOChannel::create_from_fd(stderr_fd);
        stderr_pipe->set_close_on_unref(true);
        stderr_fd = -1;

        Glib::RefPtr<Glib::IOSource> output_available = 
          stderr_pipe->create_watch(Glib::IO_IN|Glib::IO_PRI);
          
        output_available->attach(main_context);
        output_available->connect(sigc::mem_fun(this,&ProcessRunner::on_stderr_available));
      }
      
      main_loop->run();
      
      cleanup();

#ifdef WIN32
      return status;
#else
      return (int)(char)WEXITSTATUS(status);
#endif
    }
    catch(...)
    {
      cleanup();
      throw;
    }
  }
  
  bool ProcessRunner::on_stdin_wanted(Glib::IOCondition condition)
  {
    // read data from the proc->stdin_handler, and forwards it to
    // the stdin pipe of the child process.
    
    // if there's no input left in the buffer, read some new data from
    // the provider
    if(input_read_ptr == input_data_size)
    {
      proc->stdin_provider->provide_input(input_buffer,input_buffer_size,input_data_size);
      input_read_ptr = 0;
      
      // end of file. close the pipe
      if(input_data_size == -1)
      {
        stdin_pipe.clear();
        if(stdin_fd)
        {
          close(stdin_fd);
          stdin_fd = 0;
        }
        return false;
      }
    }
    
  again:
    gsize num_bytes_written;
    Glib::IOStatus status = stdin_pipe->write(input_buffer + input_read_ptr,
      input_data_size - input_read_ptr,num_bytes_written);
    switch(status)
    {
    case Glib::IO_STATUS_ERROR:
    case Glib::IO_STATUS_EOF:
      assert(!"not implemented yet");
      break;
      
    case Glib::IO_STATUS_NORMAL:
      input_read_ptr = num_bytes_written;
      break;
    
    case Glib::IO_STATUS_AGAIN:
      goto again;
    }

    return true;
  }
  
  bool ProcessRunner::on_stdout_available(Glib::IOCondition condition)
  {
    return on_output_available(stdout_pipe,proc->stdout_handler);
  }

  bool ProcessRunner::on_stderr_available(Glib::IOCondition condition)
  {
    return on_output_available(stderr_pipe,proc->stderr_handler);
  }
  
  bool ProcessRunner::on_output_available(
    Glib::RefPtr<Glib::IOChannel> pipe,
    Glib::RefPtr<IProcessOutputHandler> output_handler)
  {
    char buffer[1024];
    gsize num_bytes_read;
    
  again:
    Glib::IOStatus status = pipe->read(buffer,1024,num_bytes_read);
    switch(status)
    {
    case Glib::IO_STATUS_ERROR:
      return true;
        
    case Glib::IO_STATUS_NORMAL:
      if(num_bytes_read == 0)
        return true;

#ifndef NDEBUG
      try
      {
#endif
      output_handler->handle_output(buffer,num_bytes_read);
#ifndef NDEBUG
      }
      catch(...)
      {
        assert("You can't thrown through glib!");
      }
#endif
      break;
        
    case Glib::IO_STATUS_EOF:
      return false;
        
    case Glib::IO_STATUS_AGAIN:
      goto again;
    }
    
    return true;
  }

  void ProcessRunner::on_process_quitted(GPid pid,gint status,gpointer data)
  {
    ProcessRunner *this_ptr = (ProcessRunner*)data;
    
    this_ptr->status = status;
    this_ptr->pid = 0;
    this_ptr->main_loop->quit();
  }
  
  //////////////////
  // OutputToGIOChannel
  OutputToGIOChannel::OutputToGIOChannel(Glib::RefPtr<Glib::IOChannel> channel)
  {
    this->channel = channel;
  }
  
  void OutputToGIOChannel::handle_output(char *buffer,int size)
  {
    gsize total_num_written = 0;

    while(total_num_written < (gsize)size)
    {
      gsize cur_bytes_written;
      Glib::IOStatus status = channel->write(
        buffer + total_num_written,size - total_num_written,cur_bytes_written);
      // doesn't make any sense for writing, doesn't it?
      assert(status != Glib::IO_STATUS_EOF);
      switch(status)
      {
      case Glib::IO_STATUS_ERROR:
        assert(!"not implemented yet");
        break;

      case Glib::IO_STATUS_NORMAL:
        total_num_written += cur_bytes_written;
        break;

      case Glib::IO_STATUS_AGAIN:
        // just try to write the same part again, the next iteration.
        break;
      }
    }
  }
  
  /////////////
  // InputFromGIOChannel
  InputFromGIOChannel::InputFromGIOChannel(Glib::RefPtr<Glib::IOChannel> channel)
  {
    this->channel = channel;
  }
  
  void InputFromGIOChannel::provide_input(char *buffer,int max_size,int &actual_size_provided)
  {
  again:
    gsize num_bytes_read;
    Glib::IOStatus status = channel->read(buffer,max_size,num_bytes_read);
    switch(status)
    {
    case Glib::IO_STATUS_ERROR:
      assert(!"not implemented yet");
      break;
      
    case Glib::IO_STATUS_NORMAL:
      actual_size_provided = (int)num_bytes_read;
      return;
      
    case Glib::IO_STATUS_EOF:
      actual_size_provided = -1;
      break;
      
    case Glib::IO_STATUS_AGAIN:
      goto again;
    }
  }
}

